package com.leetcode.array_matrix;

/**
 * @author Dennis Li
 * @date 2020/9/8 10:39
 */
public class FindMedianSortedArrays_4 {

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length;
        int left = (len1 + len2 + 1) / 2;
        int right = (len1 + len2 + 2) / 2;

        return (findKth(nums1, 0, len1 - 1, nums2, 0, len2 - 1, left) +
                findKth(nums1, 0, len1 - 1, nums2, 0, len2 - 1, right)) * 0.5;
    }

    public int findKth(int[] nums1, int s1, int e1, int[] nums2, int s2, int e2, int k) {
        int len1 = e1 - s1 + 1, len2 = e2 - s2 + 1;
        // 保证左边的长度较小
        if (len1 > len2) {
            return findKth(nums2, s2, e2, nums1, s1, e1, k);
        }
        // 左边数组的所有数组都比中位数小
        if (len1 == 0) return nums2[s2 + k - 1];
        // 两个边界处理，还有在k == 1时也需要记录，不然可能出现i j 小于0的情况
        if (k == 1) return Math.min(nums1[s1], nums2[s2]);

        // 每次移动k/2个距离进行比较
        // 如果比len大了，则直接移动到len
        int i = s1 + Math.min(len1, k >> 1) - 1;
        int j = s2 + Math.min(len2, k >> 1) - 1;

        // 较小的那一部分进行移动
        return nums1[i] > nums2[j] ?
                findKth(nums1, s1, e1, nums2, j + 1, e2, k - (j - s2 + 1))
                : findKth(nums1, i + 1, e1, nums2, s2, e2, k - (i - s1 + 1));
    }

    public double findMedianSortedArrays2(int[] nums1, int[] nums2) {
        int leftLength = nums1.length;
        int rightLength = nums2.length;
        // 为了保证第一个数组比第二个数组小(或者相等)
        if (leftLength > rightLength) {
            return findMedianSortedArrays(nums2, nums1);
        }
        // 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
        // 两个数组长度之和为偶数时，当在长度之和上+1时，由于整除是向下取整，所以不会改变结果
        // 两个数组长度之和为奇数时，按照分割线的左边比右边多一个元素的要求，此时在长度之和上+1，就会被2整除，会在原来的数
        //的基础上+1，于是多出来的那个1就是左边比右边多出来的一个元素
        int totalLeft = (leftLength + rightLength + 1) / 2;
        // 在 nums1 的区间 [0, leftLength] 里查找恰当的分割线，
        // 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
        int left = 0;
        int right = leftLength;
        // nums1[i - 1] <= nums2[j]
        //  此处要求第一个数组中分割线的左边的值 不大于(小于等于) 第二个数组中分割线的右边的值
        // nums2[j - 1] <= nums1[i]
        //  此处要求第二个数组中分割线的左边的值 不大于(小于等于) 第一个数组中分割线的右边的值
        // 循环条件结束的条件为指针重合，即分割线已找到
        while (left < right) {
            // 二分查找，此处为取第一个数组中左右指针下标的中位数，决定起始位置
            // 此处+1首先是为了不出现死循环，即left永远小于right的情况
            // left和right最小差距是1，此时下面的计算结果如果不加1会出现i一直=left的情况，而+1之后i才会=right
            // 于是在left=i的时候可以破坏循环条件，其次下标+1还会保证下标不会越界，因为+1之后向上取整，保证了
            // i不会取到0值，即i-1不会小于0
            // 此时i也代表着在一个数组中左边的元素的个数
            int i = left + (right - left + 1) / 2;
            // 第一个数组中左边的元素个数确定后，用左边元素的总和-第一个数组中元素的总和=第二个元素中左边的元素的总和
            // 此时j就是第二个元素中左边的元素的个数
            int j = totalLeft - i;
            // 此处用了nums1[i - 1] <= nums2[j]的取反，当第一个数组中分割线的左边的值大于第二个数组中分割线的右边的值
            // 说明又指针应该左移，即-1
            if (nums1[i - 1] > nums2[j]) {
                // 下一轮搜索的区间 [left, i - 1]
                right = i - 1;
                // 此时说明条件满足，应当将左指针右移到i的位置，至于为什么是右移，请看i的定义
            } else {
                // 下一轮搜索的区间 [i, right]
                left = i;
            }
        }
        // 退出循环时left一定等于right，所以此时等于left和right都可以
        // 为什么left一定不会大于right?因为left=i。
        // 此时i代表分割线在第一个数组中所在的位置
        // nums1[i]为第一个数组中分割线右边的第一个值
        // nums[i-1]即第一个数组中分割线左边的第一个值
        int i = left;
        // 此时j代表分割线在第二个数组中的位置
        // nums2[j]为第一个数组中分割线右边的第一个值
        // nums2[j-1]即第一个数组中分割线左边的第一个值
        int j = totalLeft - i;
        // 当i=0时，说明第一个数组分割线左边没有值，为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断，所以将它设置为int的最小值
        int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        // 等i=第一个数组的长度时，说明第一个数组分割线右边没有值，为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断，所以将它设置为int的最大值
        int nums1RightMin = i == leftLength ? Integer.MAX_VALUE : nums1[i];
        // 当j=0时，说明第二个数组分割线左边没有值，为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断，所以将它设置为int的最小值
        int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        // 等j=第二个数组的长度时，说明第二个数组分割线右边没有值，为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断，所以将它设置为int的最大值
        int nums2RightMin = j == rightLength ? Integer.MAX_VALUE : nums2[j];
        // 如果两个数组的长度之和为奇数，直接返回两个数组在分割线左边的最大值即可
        if (((leftLength + rightLength) % 2) == 1) {
            return Math.max(nums1LeftMax, nums2LeftMax);
        } else {
            // 如果两个数组的长度之和为偶数，返回的是两个数组在左边的最大值和两个数组在右边的最小值的和的二分之一
            // 此处不能被向下取整，所以要强制转换为double类型
            return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
        }
    }


    static class Solution {

        public double findMedianSortedArrays2(int[] nums1, int[] nums2) {
            int len1 = nums1.length, len2 = nums2.length;
            int left = (len1 + len2 + 1) >> 1;
            int right = (len1 + len2 + 2) >> 1;
            return (findKthMin(nums1, 0, len1 - 1, nums2, 0, len2 - 1, left)
                    + findKthMin(nums1, 0, len1 - 1, nums2, 0, len2 - 1, right)) * 0.5;
        }

        private double findKthMin(int[] nums1, int s1, int e1, int[] nums2, int s2, int e2, int k) {
            int len1 = e1 - s1 + 1, len2 = e2 - s2 + 1;
            if (len1 > len2) {
                return findKthMin(nums2, s2, e2, nums1, s1, e1, k);
            }

            if (len1 == 0) return nums2[s2 + k - 1];

            if (k == 1) return Math.min(nums1[s1], nums2[s2]);

            int i = s1 + Math.min(len1, k >> 1) - 1;
            int j = s2 + Math.min(len2, k >> 1) - 1;

            if (nums1[i] > nums2[j]) {
                return findKthMin(nums1, s1, e1, nums2, j + 1, e2, k - (j - s2 + 1));
            } else {
                return findKthMin(nums1, i + 1, e1, nums2, s2, e2, k - (i - s1 + 1));
            }
        }

    }
}
